/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: ATronRenderingControl.java,v $
 *
 */

package com.cidero.bridge.audiotron;

import java.util.Observable;
import java.util.Observer;
import java.util.logging.Logger;

import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.StateVariable;
import org.cybergarage.upnp.device.InvalidDescriptionException;

import com.cidero.upnp.RenderingControl;
import com.cidero.util.MyMath;

/**
 * AudioTron UPnP Bridge RenderingControl Service class. 
 *
 */
public class ATronRenderingControl extends RenderingControl
                                   implements Observer
{
  private static Logger logger = 
    Logger.getLogger("com.cidero.bridge.audiotron");

  ATronMediaRenderer mediaRenderer;
  
  /**
   * Creates a new <code>RenderingControl</code> instance.
   *
   */
  public ATronRenderingControl( ATronMediaRenderer mediaRenderer )
    throws InvalidDescriptionException
  {
    super( mediaRenderer );

    logger.fine("Entered ATronRenderingControl constructor");

    this.mediaRenderer = mediaRenderer;

    mediaRenderer.getStateModel().addObserver(this);

    initializeStateVariables();

    logger.fine("Leaving ATronRenderingControl constructor");
  }

  /**
   *  Initialize state variables. Note that required state variables 
   *  have been given 'reasonable' default values in the base class
   *  version of this routine 
   */
  public void initializeStateVariables()
  {
    setStateVariable("Volume", "50" );
    setStateVariable("Mute", "0" );
  }
  
  /**
   *  Set Audiotron volume
   *
   *  Note that GetVolume action in base class reads/returns the state 
   *  variable, so no custom version of actionGetVolume() is needed here
   */
  public boolean actionSetVolume( Action action )
  {
    // Volume is 0-100
    String volume = action.getArgumentValue("DesiredVolume");  
    logger.fine("SetVolume: Entered - DesiredVolume = " + volume );

    // Convert volume to the appropriate Audiotron DB setting
    int volumeDB = convertVolumeToVolumeDB( volume );
    logger.fine("SETTING ATRON VOL TO " + volumeDB + "!!!");
    
    String response = 
      mediaRenderer.send( "/apicmd.asp?cmd=volume&arg=" + volumeDB,
                             ATronMediaRenderer.DEFAULT_TIMEOUT );
    if( response == null )
    {
      logger.warning( "actionSetVolume: timeout");
      return false;
    }
    
    // Update UPnP state variable. (triggerss LastChange event message)
    updateStateVariable( "Volume", volume );

    return true;
  }

  public boolean actionSetMute( Action action )
  {
    logger.fine("SetMute: Entered " );

    String response;
    String mute = action.getArgumentValue( "DesiredMute" );

    if( mute.equals("1") ) {
      response = mediaRenderer.send( "/apicmd.asp?cmd=mute&arg=1",
                                     ATronMediaRenderer.DEFAULT_TIMEOUT );
    } else {
      response = mediaRenderer.send( "/apicmd.asp?cmd=mute&arg=-1",
                                     ATronMediaRenderer.DEFAULT_TIMEOUT );
    }

    if( response == null )
      return false;

    updateStateVariable( "Mute", mute );
    
    return true;
  }

  /**
   *  RenderingControl is an observer of ATronStateModel so that it can
   *  pick up state changes made using the ATron front panel or remote.
   *  If the ATronStateModel info differs from the UPnP state variables,
   *  update the state variables and fire off a LastChange event
   */
  public void update( Observable model, Object arg )
  {
    ATronStateModel atronState = (ATronStateModel)model;
    
    // Update the UPnP state variables to match the state model
    
    //
    // Volume
    //
    // Check if volume in status object differs from that in UPnP 
    // state variable. If so, update state variable
    //
    // Note: volume reported from Audiotron is in dB (-96 to 0 dB),
    // and the mapping to the UPnP scale of 0-100 is nonlinear.
    // Since integers are used for both representations, information
    // is lost in the original conversion to dB. Going in the
    // the reverse direction, an integer dB value can therefore map to more 
    // than one integer value. To avoid false detection of modified 
    // volume, we need to check if the current UPnP volume maps to 
    // the dB volume - if not, a true change occurred.
    // 
    StateVariable stateVar = getService().getStateVariable( "Volume" );
    String currVolume = stateVar.getValue();
    logger.fine("Update: curr state variable volume: " + currVolume );
    int currVolumeDB = convertVolumeToVolumeDB( currVolume );

    if( atronState.getVolume() != currVolumeDB )
    {
      currVolume = convertVolumeDBToVolumeString( atronState.getVolume() );
      updateStateVariable( "Volume", currVolume );  // sends LastChangeEvent
    }

    // Mute
    updateStateVariable( "Mute", atronState.getMuteString() );
  }
  
  /**
   *  Return volume in Audiotron units of 0 to -96dB.  Conversion 
   *  is done on log scale to make volume of 50 sound 'half' as loud 
   *  to the human ear.
   *
   *  Derviation of below:
   *
   *  Want to find X such that
   *
   *   X * log10( volume ) = 96    when volume == 100  (full range)
   *
   *  so X = 48
   */
  public int convertVolumeToVolumeDB( String volumeString )
  {
    double volume =  Double.parseDouble( volumeString );

    int volumeDB =  (int)( 48.0 * MyMath.log10(volume) ) - 96;
    if( volumeDB < -96 )
      volumeDB = -96;
    if( volumeDB > 0 )
      volumeDB = 0;
    
    return volumeDB;
  }

  /**
   *  Convert volume from units of DB (-96 to 0 dB) to volume percent 
   *  (0-100)
   */
  public String convertVolumeDBToVolumeString( int volumeDB )
  {
    int volume = (int) ( Math.pow( 10.0, ((double)volumeDB + 96.0)/48.0 ) );
    return Integer.toString( volume );
  }

}

